package com.gome.boot.config.dubbo.ext;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import com.alibaba.dubbo.common.utils.ConcurrentHashSet;
import com.alibaba.dubbo.common.utils.ReflectUtils;
import com.alibaba.dubbo.config.*;
import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.dubbo.config.spring.ReferenceBean;
import com.alibaba.dubbo.config.spring.ServiceBean;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * AnnotationBean扫描扩展.
 * <br/>修复dubbo2.5.5版本的注解扫描问题.
 *
 * @author baoxiufeng
 */
public class AnnotationBeanExt extends AbstractConfig implements DisposableBean, BeanFactoryPostProcessor, BeanPostProcessor, ApplicationContextAware {
    private static final Logger logger = LoggerFactory.getLogger(AnnotationBeanExt.class);
    private String annotationPackage;
    private String[] annotationPackages;
    private final Set<ServiceConfig<?>> serviceConfigs = new ConcurrentHashSet();
    private final ConcurrentMap<String, ReferenceBean<?>> referenceConfigs = new ConcurrentHashMap();
    private ApplicationContext applicationContext;

    public AnnotationBeanExt() {
    }

    public String getPackage() {
        return this.annotationPackage;
    }

    public void setPackage(String annotationPackage) {
        this.annotationPackage = annotationPackage;
        this.annotationPackages = isNotEmpty(annotationPackage) ? Constants.COMMA_SPLIT_PATTERN.split(annotationPackage) : null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (isNotEmpty(annotationPackage) && beanFactory instanceof BeanDefinitionRegistry) {
            try {
                Class e = ReflectUtils.forName("org.springframework.context.annotation.ClassPathBeanDefinitionScanner");
                Object scanner = e.getConstructor(new Class[]{BeanDefinitionRegistry.class, Boolean.TYPE}).newInstance(new Object[]{beanFactory, Boolean.valueOf(true)});
                Class filterClass = ReflectUtils.forName("org.springframework.core.type.filter.AnnotationTypeFilter");
                Object filter = filterClass.getConstructor(new Class[]{Class.class}).newInstance(new Object[]{Service.class});
                Method addIncludeFilter = e.getMethod("addIncludeFilter", new Class[]{ReflectUtils.forName("org.springframework.core.type.filter.TypeFilter")});
                addIncludeFilter.invoke(scanner, new Object[]{filter});
                String[] packages = Constants.COMMA_SPLIT_PATTERN.split(this.annotationPackage);
                Method scan = e.getMethod("scan", new Class[]{String[].class});
                scan.invoke(scanner, new Object[]{packages});
            } catch (Throwable t) {
                ;
            }
        }
    }

    @Override
    public void destroy() throws Exception {
        for (ServiceConfig serviceConfig : serviceConfigs) {
            try {
                serviceConfig.unexport();
            } catch (Throwable t) {
                logger.error(t.getMessage(), t);
            }
        }
        for (ReferenceConfig referenceConfig : referenceConfigs.values()) {
            try {
                referenceConfig.destroy();
            } catch (Throwable t) {
                logger.error(t.getMessage(), t);
            }
        }
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (!isMatchPackage(bean)) {
            return bean;
        } else {
            Class clazz = bean.getClass();
            if (isProxyBean(bean)) {
                clazz = AopUtils.getTargetClass(bean);
            }
            Service service = (Service) clazz.getAnnotation(Service.class);
            if (service != null) {
                ServiceBean serviceConfig = new ServiceBean(service);
                if (Void.TYPE.equals(service.interfaceClass()) && "".equals(service.interfaceName())) {
                    if (clazz.getInterfaces().length <= 0) {
                        throw new IllegalStateException("Failed to export remote service class " + clazz.getName() + ", cause: The @Service undefined interfaceClass or interfaceName, and the service class unimplemented any interfaces.");
                    }
                    serviceConfig.setInterface(clazz.getInterfaces()[0]);
                }
                if (applicationContext != null) {
                    serviceConfig.setApplicationContext(applicationContext);
                    if (isNotEmpty(service.registry())) {
                        serviceConfig.setRegistries(getRegistries(service.registry()));
                    }
                    if (isNotEmpty(service.provider())) {
                        serviceConfig.setProvider(applicationContext.getBean(service.provider(), ProviderConfig.class));
                    }
                    if (isNotEmpty(service.monitor())) {
                        serviceConfig.setMonitor(applicationContext.getBean(service.monitor(), MonitorConfig.class));
                    }
                    if (isNotEmpty(service.application())) {
                        serviceConfig.setApplication(applicationContext.getBean(service.application(), ApplicationConfig.class));
                    }
                    if (isNotEmpty(service.module())) {
                        serviceConfig.setModule(applicationContext.getBean(service.module(), ModuleConfig.class));
                    }
                    if (isNotEmpty(service.provider())) {
                        serviceConfig.setProvider(applicationContext.getBean(service.provider(), ProviderConfig.class));
                    }
                    if (isNotEmpty(service.protocol())) {
                        List<ProtocolConfig> protocols = new ArrayList<ProtocolConfig>();
                        for (String protocolId : service.protocol()) {
                            protocols.add(applicationContext.getBean(protocolId, ProtocolConfig.class));
                        }
                        serviceConfig.setProtocols(protocols);
                    }
                    try {
                        serviceConfig.afterPropertiesSet();
                    } catch (RuntimeException re) {
                        throw re;
                    } catch (Exception ex) {
                        throw new IllegalStateException(ex.getMessage(), ex);
                    }
                }
                serviceConfig.setRef(bean);
                serviceConfigs.add(serviceConfig);
                serviceConfig.export();
            }
            return bean;
        }
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (!isMatchPackage(bean)) {
            return bean;
        } else {
            Class clazz = bean.getClass();
            if (isProxyBean(bean)) {
                clazz = AopUtils.getTargetClass(bean);
            }
            Reference reference = null;
            Object value = null;
            for (Method m : clazz.getMethods()) {
                String mName = m.getName();
                if (mName.length() > 3 && mName.startsWith("set") && m.getParameterTypes().length == 1 && Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers())) {
                    try {
                        reference = m.getAnnotation(Reference.class);
                        if (reference != null) {
                            value = refer(reference, m.getParameterTypes()[0]);
                            if (value != null) {
                                m.invoke(bean, new Object[]{value});
                            }
                        }
                    } catch (Exception e) {
                        throw new BeanInitializationException("Failed to init remote service reference at method " + mName + " in class " + bean.getClass().getName(), e);
                    }
                }
            }
            for (Field f : clazz.getDeclaredFields()) {
                try {
                    if (!f.isAccessible()) {
                        f.setAccessible(true);
                    }
                    reference = f.getAnnotation(Reference.class);
                    if (reference != null) {
                        value = this.refer(reference, f.getType());
                        if (value != null) {
                            f.set(bean, value);
                        }
                    }
                } catch (Exception e) {
                    throw new BeanInitializationException("Failed to init remote service reference at filed " + f.getName() + " in class " + bean.getClass().getName(), e);
                }
            }
            return bean;
        }
    }

    private Object refer(Reference reference, Class<?> referenceClass) {
        String interfaceName;
        if (!"".equals(reference.interfaceName())) {
            interfaceName = reference.interfaceName();
        } else if (!Void.TYPE.equals(reference.interfaceClass())) {
            interfaceName = reference.interfaceClass().getName();
        } else {
            if (!referenceClass.isInterface()) {
                throw new IllegalStateException("The @Reference undefined interfaceClass or interfaceName, and the property type " + referenceClass.getName() + " is not a interface.");
            }
            interfaceName = referenceClass.getName();
        }

        String key = reference.group() + "/" + interfaceName + ":" + reference.version();
        ReferenceBean referenceConfig = referenceConfigs.get(key);
        if (referenceConfig == null) {
            referenceConfig = new ReferenceBean(reference);
            if (Void.TYPE.equals(reference.interfaceClass()) && "".equals(reference.interfaceName()) && referenceClass.isInterface()) {
                referenceConfig.setInterface(referenceClass);
            }
            if (applicationContext != null) {
                referenceConfig.setApplicationContext(applicationContext);
                if (isNotEmpty(reference.registry())) {
                    referenceConfig.setRegistries(getRegistries(reference.registry()));
                }
                if (isNotEmpty(reference.monitor())) {
                    referenceConfig.setMonitor(applicationContext.getBean(reference.monitor(), MonitorConfig.class));
                }
                if (isNotEmpty(reference.application())) {
                    referenceConfig.setApplication(applicationContext.getBean(reference.application(), ApplicationConfig.class));
                }
                if (isNotEmpty(reference.module())) {
                    referenceConfig.setModule(applicationContext.getBean(reference.module(), ModuleConfig.class));
                }
                if (isNotEmpty(reference.consumer())) {
                    referenceConfig.setConsumer(applicationContext.getBean(reference.consumer(), ConsumerConfig.class));
                }
                try {
                    referenceConfig.afterPropertiesSet();
                } catch (RuntimeException e) {
                    throw e;
                } catch (Exception e) {
                    throw new IllegalStateException(e.getMessage(), e);
                }
            }
            referenceConfigs.putIfAbsent(key, referenceConfig);
            referenceConfig = referenceConfigs.get(key);
        }

        return referenceConfig.get();
    }

    private List<RegistryConfig> getRegistries(String[] registry) {
        List<RegistryConfig> registries = new ArrayList<RegistryConfig>(registry.length);
        for (String registryId : registry) {
            if (isNotEmpty(registryId)) {
                registries.add(applicationContext.getBean(registryId, RegistryConfig.class));
            }
        }
        return registries;
    }

    private boolean isMatchPackage(Object bean) {
        if (isNotEmpty(annotationPackages)) {
            Class clazz = bean.getClass();
            if (this.isProxyBean(bean)) {
                clazz = AopUtils.getTargetClass(bean);
            }
            String beanClassName = clazz.getName();
            for (String pkg : annotationPackages) {
                if (beanClassName.startsWith(pkg)) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }

    private boolean isProxyBean(Object bean) {
        return AopUtils.isAopProxy(bean);
    }

    /**
     * 判定指定字符串对象是否非空.
     *
     * @param cs 字符串对象
     * @return 判定结果（true：非空，false：空）
     */
    public static boolean isNotEmpty(CharSequence cs) {
        return cs != null && cs.toString().trim().length() > 0;
    }

    /**
     * 判定指定数组是否非空.
     *
     * @param array 待判定的数据对象
     * @return 判定结果（true：非空、false：空）
     */
    public static boolean isNotEmpty(Object[] array) {
        return array != null && array.length > 0;
    }
}
